#!/user/bin/env python
# -*- coding: utf-8 -*-
''' 
Author: Sjy
LastEditTime: 2024-09-06 10:01:50
Description: extree 提供自定义树控件,展示数据以及相关操作
事件执行逻辑:
    1. 域 一旦给某个节点(一般是文件夹节点)设置了域属性后则会截断全部事件的冒泡行为,即传递到当前节点并执行完为止不会继续向上冒泡
    2. 设置事件传播(propagation)规则,可设置参数如下,注意这些某些事件可以组合设置
        "default"/False/None: 传递,缺省值,仅传递到当前节点并执行不会向上冒泡. 
            1. 如果是触发,那么先执行cell对应事件然后执行node对用事件,然后停止冒泡
            2. 如果是传播,则传播到此节点时执行完node事件后停止冒泡
        "no":不传递任何事件
            1. 如果是触发事件,那么只会执行当前节点cell的对应事件不会执行node对应事件
            2. 如果是传播事件,则传播到此节点时不会执行任何事件且不会继续向上传播
        "cell": 传递,只传递cell事件,不传递节点事件
            1. 无论是否为触发还是传播都只会执行cell对应事件并向上传递
        "node": 传递,只传递节点事件,不传递cell事件
            1. 无论是否为触发还是传播都只会执行node对应事件并向上传递
        true: 传递,全部事件都会传递
            1. 无论是否为触发还是传播全部执行完毕后向上传递
        "ignore": 传递但忽略当前,即会继续项父节点或者tree传递事件,但是当前节点的事件不会触发
            1. 如果是触发,则执行当前节点的cell和node对应的事件,然后向上传递
            2. 如果是传播,则传播到此节点时直接略过当前节点继续向上传递(不执行当前节点的任意事件)
        "igonreRbtnClick": 传递但忽略右键点击事件,注意此行为只有当前节点定义了右键点击事件时才有效
            1. 无论是触发还是传播,右键点击会执行右键菜单命令忽略右键点击事件,然后向上传递,其他类型的事件的传递规则与True相同
        "igonreRmenu": 传递但忽略右键菜单事件,注意此行为只有当前节点定义了右键菜单事件时才有效
            1. 无论是触发还是传播,右键点击会执行右键命令忽略右键菜单事件,然后向上传递,他类型的事件的传递规则与True相同
    3. 设置的事件函数本身也可以控制事件的传递规则,在参数params中会提供event 属性执行 event.stop() 可以终止当前事件类型的传递
        比如鼠标点击时会触发 itemPressed 和 itemClicked, 如果在 itemPressed 事件执行时设置了 event.stop() 可以终止后续此类型的事件传播,但是 itemClicked 事件仍然会执行 可通过 event.stopAll() 终止后续所有事件的传播


事件使用注意事项:
    1. 如果想要使用某种类型事件则必须开启对应的默认事件,否则不会触发
    2. 尽量不要使用自己connect事件,因为不受控制
    3. 鼠标类事件还分 lbtn,rbtn,mbtn 三种,分别对应鼠标左键,右键,中键点击事件,例如点击事件可以分为 lbtnClick, rbtnClick, mbtnClick, click 四种其中click是鼠标任意键点击都会触发
        qt事件执行顺序 itemPressed -> itemClicked -> itemDoubleClicked -> itemActivated
            注意： rmenu > rbtnClicked  一般情况下启用了右键菜单就不要在启用右键事件了
            注意 qt默认只有鼠标左键点击才会触发 itemClicked 事件 所以如果想要实现右键单击 使用 mousePressEvent 重写
        假设鼠标左键点击那么触发事件的在当前树为
            只开启了树的点击事件itemClicked
                1. 自定义cell自身事件
                2. 节点列  绑定的点击事件 lbtnClick -> click (先执行“左键点击”事件然后在执行“点击”事件)
                3. 节点    绑定的点击事件 lbtnClick -> click
                4. 父节点   ...
                5. 域     or    5. 列头     ...
                                6. 树       ...  ...
                注意 
                    1. 一旦设置了域属性就截断了事件链,不会继续向上冒泡,执行到域为止
                    2. 注意域也分节点域还是cell域,如果是cell 域那么只有当前列会受影响,其他列依然会继续冒泡
                    3. 事件冒泡是从子节点开始一直到根节点到tree本身,但是默认设置是不允许冒泡,任何节点都需要主动开启

            只开启了树的点击事件itemClicked和按下事件itemPressed,那么会先执行pressed事件然后在执行clicked事件
                1. 自定义cell自身事件
                2. 节点列  绑定的按下事件 lbtnPressed -> pressed
                ... 
                按下事件执行完毕后执行点击事件,cell自身的事件不需要重复执行
                8. 节点列  绑定的点击事件 lbtnClick -> click
                9. 节点    绑定的点击事件 lbtnClick -> click
        注意：在事件执行序列中如果,某个事件的返回了中止信号则后续事件不会执行(仅限当前事件)
    4.
'''

'''功能点如下

核心-允许二次开发定制
按指定的数据结构即可显示数据,不需要关心显示的细节,只需要关心数据结构即可
后续考虑 checkbox 处理,暂时不考虑

事件
    1. 如果想要使用某种类型的事件,那么必须启用对应的默认事件才行,否则不会触发
        禁止自身直连事件,如果需要自定义事件,请使用 _add_action 方法添加 或者在节点或者标题中添加事件

    cell自定义widget事件->cell绑定事件（节点列）->节点绑定事件(一个数据行)->父->域绑定事件(域时特殊的节点,相当于把节点打了标记)->列头绑定事件->树绑定事件
        因为cell 允许定制,其可能是任意其他组件,因此它自身的事件优先级最高,其次是cell绑定事件,然后是节点绑定事件,以此类推
        cell绑定事件以及节点的绑定事件 是通过传入的数据行中的 events 属性设置与列头类似,区别是如果存在以列头的field做key对应的列表,那么表示存在指定列对应的事件集合
            events:{
                # 节点交互事件
                lbtnClick: 鼠标左键点击
                rbtnClick: 鼠标右键点击
                mbtnClick: 鼠标中键点击
                click: 点击事件,只要点击就会触发(鼠标任意键点击都会触发)
                ...
                # cell绑定事件 
                [field0]:{ # 假设field0 是第一列的field字段对应的值, 表示当前节点当前列的事件列表
                    lbtnClick: 鼠标左键点击
                    rbtnClick: 鼠标右键点击
                    mbtnClick: 鼠标中键点击
                    click: 点击事件,只要点击就会触发(鼠标任意键点击都会触发)
                    ...
                }

            }
        列头的绑定事件 是通过传入的列头参数中的 events 属性设置例如
            events:{ #列默认事件列表, 也可以是函数,函数则会传入,
                # 交互事件
                lbtnClick: 鼠标左键点击
                rbtnClick: 鼠标右键点击
                mbtnClick: 鼠标中键点击
                click: 点击事件,只要点击就会触发(鼠标任意键点击都会触发)
                ...
            } 
        树的绑定事件 可以通过 _set_lbtn_click, _set_click, _set_dbclick ... 等方法单独设置 也可以通过 _set_actions 整理设置


显示  =====================================》
显示-标题
> 标题显示/不显示
> 标题内容自定义
> 标题样式自定义
显示-列
> 多列/单列
> 根据列头定义当前列每行数据的单元格类型
> 根据列头定义当前列每行数据单元格的宽度以及是否自适应

显示-数据行
> 图标/文字/图标+文字
> 复选框/单选框/无
> 统计当前分支的子节点个数



操作  =====================================》
> 选中/不选中/部分选中, 暂不考虑(需要考虑有些节点需要,有些节点不需要)
> 各类事件的处理, 比如点击,双击,右键点击、右键菜单等。(不同列的单元格同一类型事件处理方式可能不同) 
    1. 有些节点的事件是可以继承的,比如子节点继承父节点的右键菜单,
    2. 某个分支下 叶子节点的菜单是统一的,分支节点的右键菜单是统一的。
    3. 有些节点没有某个事件
    在添加,拷贝等新增节点时要考虑是否事件继承于当前

> 拖拽/拖拽放置
> 搜索/过滤/排序 以及相关操作后的恢复,显示信息的恢复


数据处理
> 自定义数据格式,以及可以解析数据的程序,可以显示,取出数据, 以及自动初始化相关事件
> 可以在任意节点加载提供模板格式的数据
> 可以根据相关信息快速定位到相关节点,做到动态更新等
> 树要有多工程概念,相同工程显示的节点名字相同,但是要做到动态更新时需要考虑到更新到准确的工程位置
    因此要考虑到节点关联项:
    > 作用域(可以是工程,也可以是其他)
    > 节点信息(包括不同列)
        > 节点展示信息如图标,文字,提示信息等
        > 节点样式 颜色,字体等
        > 节点事件 如点击,双击,右键点击、右键菜单等
    > 节点数据 实际存取的数据,
'''


'''变量数据结构如下
__headers = [  注意在列头设置的样式和事件是所有cell的列默认样式和事件
    {# 第一列
        title: 显示的标题, 标题必须存在,title如果定义为True 则按 excel列顺序显示(待议)
        field: 数据字段名,没有则按列索引为字段名
        checkable: 是否开启复选框,注意 这是与colType搭配的可搭配的项有 icon,iconText,text
        radioable: 是否开启单选框,注意 这是与colType搭配的可搭配的项有 icon,iconText,text

        colType: 列默认单元格类型(可被子节点覆盖掉),
            label(默认),icon 图标类型,iconLabel 图标+文字类型, text可编辑文本,checkbox 复选框,radio 单选框,comboBox 下拉框, custom 自定义(自定义是传入自定义的widget,仍然可被具体节点覆盖),
        styles: {
            width: 列宽度 100
            autoWidth: 是否自适应宽度 true/false, 注意如果自适应宽度则width无效
            align: 对齐方式 left/center/right
            bgcolor: 背景色
            color: 字体颜色
            font: 字体
        } or function  列默认样式, 也可以是函数,根据数据动态返回样式
        events:{ #列默认事件列表, 也可以是函数,函数则会传入,
            # 交互事件
            lbtnClick: 鼠标左键点击
            rbtnClick: 鼠标右键点击
            mbtnClick: 鼠标中键点击
            click: 点击事件,只要点击就会触发(鼠标任意键点击都会触发)
            dbClick: 双击事件

            ...
            # 生命周期事件
            onInit 初始化事件,仅节点初次创建时调用
            onDestroy销毁事件,节点销毁时调用
        },
        propagations:{ # 定义具体事件的传播规则
        }
    }
]

__gids = {
    uuid4:{
        widgetItem,
        注意外部存放显示对应信息 比如第一列的field值是name 那么如果数据中有对应的值则可以直接显示例如
        name: "xxx", # 对应列头的 field 的值 这样就会显示在第一列

        data:{}, # data 此key一般用于作为与后台数据库交互的内容,不显示在树上
        styles:{
            [headerName0]: {}   标题名对应的样式表,可以根据标题名确定当前列单元格的样式
            bgcolor: "" 直接定义样式,表示该样式全行通用,也适用于仅有1列的情况
        },
        events:{
            [headerName0]: {}   标题名对应的样式表,可以根据标题名确定当前列单元格的样式
            onDbClick:{直接定义事件,表示该事件全行通用,也适用于仅有1列的情况
                """双击事件"""
            } 
        }
    }
}
'''
__all__= []

# 导入相关模块
import uuid
# 引入QT相关模块
from PyQt5 import QtWidgets, QtCore, QtGui
# 引入自定义模块
try:
    import gui.components.utils as utils
    import gui.components.timers as timers
except:
    """在 根目录下单独测试"""
    import utils as utils
    import timers as timers


global FLAGS 
FLAGS = {
    "pressAccept": QtCore.Qt.UserRole + 1, # 鼠标按压事件标志
}
global DEFAULT_ACTION_KEYS # 操作tree提供默认事件列表可以设置启用还是不启用
DEFAULT_ACTION_KEYS = ["itemClicked", "itemDoubleClicked", "itemActivated", "itemPressed", "currentItemChanged", "itemSelectionChanged", "customContextMenuRequested"]  #  "itemSelectionChanged", "currentItemChanged" 等事件

class TreeNode(QtWidgets.QTreeWidgetItem):
    def __init__(self, gid, *args, **kwargs):
        super(TreeNode, self).__init__(*args, **kwargs)
        self.setData(0, QtCore.Qt.UserRole, gid)
        self.setFlags( self.flags() |  QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
class _Tree(QtWidgets.QTreeWidget):
    def __init__(self, parent: QtWidgets.QWidget = None):
        super(_Tree, self).__init__(parent)
        self.setUniformRowHeights(True) # 所有节点的高度保持相同
        self.setRootIsDecorated(True)   # 设置根节点 是否显示展开折叠图标  True 显示,False 不显示（仅限根节点）
        self.setHeaderHidden(False)     # 默认不隐藏头 显示头部标题
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)  # 打开右键菜单的策略
    def mousePressEvent(self, event):
        # 鼠标点击事件,判断当前点击位置是否有item且满足标志否则拦截鼠标事件
        """例如设置父节点不可选中且不能执行相应的点击事件 注意配合下面的 mousePressEvent 方法使用
            pitem1 = QTreeWidgetItem(self, ["parent item 1"])
            # 设置不可选中
            pitem1.setFlags(pitem1.flags() & ~Qt.ItemIsSelectable)
            # 设置一个标识用于屏蔽鼠标事件,这样就不会触发点击事件
            pitem1.setData(0, Qt.UserRole + 1, True)
        """
        item = self.itemAt(event.pos())
        if item and item.data(0, FLAGS.get("pressAccept")):
            event.accept()
            return
        if event.button() == QtCore.Qt.RightButton or event.button() == QtCore.Qt.MiddleButton:
            item = self.itemAt(event.pos())
            if item is not None:
                # 获取当前点击的col 
                col = self.columnAt(event.pos().x())
                self.itemClicked.emit(item, col)  # 发出自定义的点击信号
        super(_Tree, self).mousePressEvent(event)
    def closeEvent(self, event):
        # 断开与 QTreeWidget 相关的所有信号-槽连接
        QtCore.QObject.disconnect(self)
        event.accept()
class BaseTree(_Tree):
    """基础树,提供全部的自定义基础功能"""
    def __init__(self, parent: QtWidgets.QWidget = None):
        super(BaseTree, self).__init__(parent)
        self.__flagDbclickEvent = False     # 双击标志,用于判断是否触发双击事件屏蔽单击事件
        self.__init_vars()              # 初始化变量
        self._init_header_sets()               # 自动初始化相关配置信息
    def _set_dbclick_event_flag(self, flag:bool):
        """设置双击标志"""
        self.__flagDbclickEvent = flag
    def _get_dbclick_event_flag(self):
        """获取双击标志"""
        return self.__flagDbclickEvent
    def __init_vars(self):
        """初始化变量"""
        self.__headers = {}         # 列头, 缓存列头信息,用于根据列头定义初始化数据行各个单元格类型以及赋值等信息
        self.__gids = {}            # 记录节点id 数据、事件、样式映射关系表,这样每个树中仅绑定数据的gid,不绑定数据,数据通过gid获取,好处是如果指导gid可以直接获取信息而不需要通过tree访问
        self.__headerTitles = []    # 列头标题
    def _init_header_sets(self):
        """初始化列头配置"""
        self.header().setStretchLastSection(False) # 设置是否铺满宽度 默认不铺满
    # gid数据信息处理  =====================================
    def _gid_item(self, gid, type:str="cache"):
        """获取gid的数据, 默认获取缓存的数据"""
        try:
            return self.__gids.get(gid, {}).get(type, {})
        except:
            return {}
    def _gid_styles(self, gid):
        """获取gid映射的样式"""
        return self._gid_item(gid, "styles")
    def _gid_events(self, gid):
        """获取gid映射的事件"""
        return self._gid_item(gid, "events")
    def _gid_data(self, gid):
        """获取gid映射的实际数据"""
        return self._gid_item(gid, "cache")
    def _gid_map(self, gid, widgetItem, data:dict={}):
        """映射gid数据"""
        if not gid or not widgetItem:return 
        if not utils.is_dict(data):
            print("userdata必须是字典类型")
            data = {}
        self.__gids[gid] = {
            "widgetItem":widgetItem,
            "data":data.get("data", {}),
            "styles":data.get("styles", {}) ,
            "events": data.get("events", {})
        }
    def _get_gids(self):
        return self.__gids
    # 标题头处理  =====================================
    def _set_headers(self, headers:list=[], render:bool=True):
        """设置数据"""
        # 先检查数据是否合法 必须有 title 和 field 这两个属性
        try:
            for header in headers:
                if not "title" in header or not "field" in header:
                    raise Exception("列头必须包含 title 和 field 两个属性")
        except Exception as e:
            print("An error occurred:", e)
            return 
        self.__headers = headers
        if render:
            self._render_headers()
    def _get_headers(self):
        """获取列头"""
        return self.__headers
    def _render_headers(self):
        """渲染头部"""
        headers = self.__headers
        # 根据列头自动确定显示的列数 暂不考虑隐藏列问题
        self.setColumnCount(len(headers))
        headerTitles = []
        for i in range(len(headers)):
            header = headers[i]
            headerTitles.append(header.get("title", ""))
            if "width" in header:
                """有宽度则固定,否则自适应"""
                self.header().resizeSection(i, header.get("width", "") )
                self.header().setSectionResizeMode(i, QtWidgets.QHeaderView.Fixed)
            else:
                self.header().setSectionResizeMode(i, QtWidgets.QHeaderView.Stretch)
        # 设置列头名称到tree 上
        self._set_header_titles(headerTitles)
    def _set_header_titles(self, titles:list=[], readerTitle:bool=True):
        """设置列头标题"""
        self.__headerTitles = titles
        readerTitle and self.setHeaderLabels(titles)
    def _get_headers_titles(self):
        """获取列头标题"""
        return self.__headerTitles
    def _title_to_col(self, title):
        """根据列头名确定当前列头所在的列"""
        return self.__headerTitles.index(title)
    def _field_to_col(self, field):
        """根据定义的字段名确定当前列头所在的列"""
        for index in range(len(self.__headers)):
            item = self.__headers[index]
            if item.get("field") == field:
                return index
        return -1
    def _col_to_field(self, col):
        """根据列确定字段名"""
        return self.__headers[col].get("field", col)
    def _col_to_event(self, col):
        """根据列确定事件"""
        return self.__headers[col].get("events", {})
    # 数据处理  =====================================
    def _set_data(self, data:list=[]):
        """给tree设置数据,注意这个函数直接重新生成tree数据,不会保留原有数据"""
        self._init_tree_items(data, pWidgetItem=None)     
    def _init_tree_items(self, data:list=[], pWidgetItem=None):
        """初始化tree显示数据
            data: 数据列表
            pWidgetItem: 父节点的widgetItem 如果未指定,则默认为直接从根部添加数据
            1. 先清理当前节点下的所有数据
            2. 添加数据
        """
        self._clear_node(pWidgetItem) # 默认没有传入根节点的时候清理
        self._init_children(pWidgetItem, data)
        not pWidgetItem and self._expand_roots() # 默认展开根节点
    def _expand_roots(self):
        """展开根节点"""
        for rootWidgetItem in self._get_root_widgetitems():
            rootWidgetItem.setExpanded(True)
    def _get_root_widgetitems(self):
        """获取根节点列表"""
        roots = []
        for i in range(0, self.topLevelItemCount()):
            """循环获取根节点"""
            rootWidgetItem = self.topLevelItem(i) # 获取顶层节点
            roots.append(rootWidgetItem)
        return roots
    def _init_children(self, pWidgetItem, data):
        """初始化子节点"""
        for item in data:
            widgetItem = self._init_tree_item(pWidgetItem or self, item)
            if 'children' in item:
                self._init_children(widgetItem, item.get('children'))
    def _init_tree_item(self, parent, data):
        """初始化节点信息,注意parent可能是tree本身
            1. 如果含有 children 属性则认为是branch节点否则未leaf节点
            2. 初始化gid
        """
        gid = uuid.uuid4()
        widgetItem = TreeNode(gid, parent)
        self._gid_map(gid, widgetItem, data)
        headers = self._get_headers()
        for i in range(len(headers)):
            header = headers[i]
            field = header.get("field", i)
            widgetItem.setText(i, str(data.get(field, "")))
        return widgetItem
    def _clear_node(self, pWidgetItem:QtWidgets.QTreeWidgetItem=None):
        """清理数据,清理子节点包含清理缓存数据和清理树节点"""
        """TODO: 清理数据时需要清理事件绑定"""
        if not pWidgetItem:
            self.clear()
            self._clear_caches()
        else:
            pWidgetItem.takeChildren()
        # 清理绑定的事件以及还有其他的数据
    def _clear_caches(self):
        """清理缓存的数据"""
        self.__gids = {}          
    def _destroy(self):
        """销毁树"""
        # 清理树本身包含的槽函数
        QtCore.QObject.disconnect(self)
        # 清楚gid绑定的事件 TODO: 待定
        # for gid, item in self.__gids.items():
        #     self._destroy_gid(gid)
        # 清楚内存中的数据
        self.clear()
        # 清理缓存数据
        self._clear_caches()
    def _destroy_gid(self, gid):
        """销毁gid绑定的事件"""
        print("_destroy_gid")
class DataTree(BaseTree):
    """仅展示数据,不带操作"""
    def __init__(self, parent: QtWidgets.QWidget = None):
        super(DataTree, self).__init__(parent)
class OpTree(BaseTree):
    """带有操作的树"""
    def __init__(self, parent: QtWidgets.QWidget = None):
        super(OpTree, self).__init__(parent)
        
        self.__enableSignals = {} # 启用的信号列表
        self.__signalActions = {} # 顶层树绑定事件集合 key 为事件名,值为事件函数
        """__events 允许设置的key如下
            lbtnClick: 鼠标左键点击
            rbtnClick: 鼠标右键点击
            mbtnClick: 鼠标中键点击
            click: 点击事件,只要点击就会触发(鼠标任意键点击都会触发)
            dbClick: 双击事件 注意只有鼠标左键有双击事件 因此默认只有一个双击事件即可
            ...
        """
    # 关联信号函数  =====================================
    def __enable_signal(self, key:str):
        """添加启用的信号"""
        self.__enableSignals[key] = True
    def _clear_enable_signals(self):
        """清除启用的默认事件集合"""
        self.__enableSignals = {}
    def _get_enable_signals(self):
        """获取启用的默认事件集合"""
        return self.__enableSignals
    def _start_enable_signals(self, useActionKeys:list=[], useAll:bool=False):
        """启动可用信号
            itemClicked: 点击事件
            itemDoubleClicked: 双击事件
            rclick: 右键点击事件
            itemActivated: 激活事件
            itemPressed: 鼠标按下事件
        useAll: 是否使用全部事件 默认为False, 如果值为True则使用全部事件 忽略第一个参数
        """
        if not useAll and not utils.is_list(useActionKeys):
            return print("OpTree._start_enable_signals(useActionKeys:list=[], useAll:bool=False) 参数 useActionKeys 必须是列表!")
        if useAll:
            useActionKeys = DEFAULT_ACTION_KEYS
        # 清楚所有的槽函数
        for key in useActionKeys or []:
            if key in DEFAULT_ACTION_KEYS:
                """key 必须在 DEFAULT_ACTION_KEYS 中否则无效"""
                eventName = f"_on_{key}"
                action =  hasattr(self, f"_on_{key}") and getattr(self, f"_on_{key}")
                
                # 注意这里不能判断 is_fun 判断是否为函数 因为通过 getattr 获取的是 method 对象
                self._set_signal_action(key, action)
                self.__enable_signal(key)
    def _set_signal_action(self, key:str, action, oldAction=None):# 
        """单独设置某个信号的槽函数,也可以删除或者覆盖
            key: 事件名,值可能如下: 
                "itemClicked"     : 点击事件
                "itemDoubleClicked"   : 双击事件
                "itemActivated" : 激活事件
                "itemPressed"   : 鼠标按下事件
                ...
            action: 事件函数
            oldAction: 旧事件函数 用来替换或者删除的旧的事件函数
        """
        self.__deal_signal(key, action, oldAction)
    def __deal_signal(self, eventName, action, oldAction):
        """处理信号,链接、删除、替换槽函数"""
        print(f"启用信号: {eventName}")
        event = getattr(self, eventName)
        try:
            if action:
                if oldAction:
                    event.disconnect(oldAction)
                event.connect(action)
                print(f"\t启用{eventName}信号成功!")
            else:
                if oldAction: 
                    event.disconnect(oldAction)
                else:
                    event.disconnect()
        except:
            utils.print_error()
    def _disconnect_singals(self):
        """清理关联的信号的槽函数"""
        enableActions = self._get_enable_signals()
        for key in enableActions.keys():
            action = getattr(self, f"_on_{key}")
            self._set_signal_action(key, None, oldAction=action)
    # 自定义信号事件处理  =================================
    def _add_action(self, key:str, action):
        """添加tree事件"""
        self.__signalActions[key] = action
    def _get_actions(self):
        """获取事件集合"""
        return self.__signalActions
    def _get_key_action(self, key:str):
        """获取事件"""
        return self.__signalActions.get(key)
    def _clear_actions(self):
        """清除事件集合"""
        self.__signalActions = {}
    def _add_pressed(self, action):
        """设置自定义tree的按压事件"""
        self._add_action("pressed", action)
    def _add_lbtn_click(self, action):
        """设置鼠标左键点击事件"""
        self._add_action("lbtnClick", action)
    def _add_rbtn_click(self, action):
        """设置鼠标右键点击事件"""
        self._add_action("rbtnClick", action)
    def _add_mbtn_click(self, action):
        """设置鼠标中键点击事件"""
        self._add_action("mbtnClick", action)
    def _add_click(self, action):
        """设置鼠标任意键点击事件"""
        self._add_action("click", action)
    def _add_dbclick(self, action):
        """设置鼠标双击事件"""
        self._add_action("dbClick", action)
    def _add_activated(self, action):
        """设置激活事件, 在pyqt5 中缺省状态下 鼠标左键双击后会触发激活事件"""
        self._add_action("activated", action)    
    # 信号对应函数 =================================
    def _on_itemPressed(self, widgetItem, col):
        """默认鼠标按下事件"""
        print("_on_itemPressed", widgetItem, col)
    def _on_itemClicked(self, widgetItem, col):
        """触发默认点击事件
            QtWidgets.QApplication.doubleClickInterval()+50 这段代码含义是 返回双击时间的间隔
                原理是要延迟执行当前的点击函数,根据qt系统双击的响应时间+50ms来判断是否触发双击事件,如果触发了双击事件则不执行单机事件否则执行单机事件
        """
        mouse_event = QtWidgets.QApplication.mouseButtons()
        QtCore.QTimer.singleShot(
            QtWidgets.QApplication.doubleClickInterval()+50, # 获取到鼠标双击的时间+50ms间隔用于判断是否触发双击事件
            lambda *args, widgetItem=widgetItem, col=col, mouse_event=mouse_event, **kwargs:self.__delay_click_action(widgetItem, col, mouse_event)
        )  
    def _on_itemDoubleClicked(self, widgetItem, col):
        """默认双击事件
            1. 仅处理左键双击事件
            2. 默认的双击事件会触发设置的自定义响应函数,如果自定义响应函数未实现则不会触发
        """
        self._set_dbclick_event_flag(True)
        self.__run_action_chain(widgetItem, col, ["dbClick"])
    def _on_itemActivated(self, widgetItem, col):
        """默认激活事件"""
        self.__run_action_chain(widgetItem, col, ["activated"])
    def _on_currentItemChanged(self, currentTreeWidgetItem, previousTreeWidgetItem):
        """自定义当前节点改变事件 由信号 currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous)
        currentTreeWidgetItem: 当前节点
        previousTreeWidgetItem: 上一个节点
        """
        self.__run_action_chain(currentTreeWidgetItem, 0, ["currentItemChanged"], params={"previousTreeWidgetItem":previousTreeWidgetItem})       
    def _on_itemSelectionChanged(self):
        """自定义当前节点选择发生变化执行的函数 由信号 itemSelectionChanged() 触发"""
        selected_items = self.selectedItems()
        for widgetItem in selected_items:
            self.__run_action_chain(widgetItem, 0, ["itemSelectionChanged"])
    def _on_customContextMenuRequested(self, point:QtCore.QPoint=None):
        """自定义右键菜单事件,传入的参数是当前点击的位置, 由信号 customContextMenuRequested(const QPoint &pos) 触发"""
        print('右键菜单', point)
        widgetItem = self.currentItem()
        col = self.currentColumn()
        if widgetItem:
            self.__run_action_chain(widgetItem, col, ["rmenu"])
    # 点击触发函数
    def __delay_click_action(self, widgetItem, col, mouse_event, *args, **kwargs):
        """延迟执行点击事件"""
        if self._get_dbclick_event_flag():
            """判断是否双击,双击则恢复默认标志,不是双击则正常执行响应单机响应事件"""
            self._set_dbclick_event_flag(False)
        else:
            self.__run_click_action(widgetItem, col, mouse_event)
    def __run_click_action(self, widgetItem, col, mouse_event):
        """运行单击事件触发行为"""
        # 需要注意的是如果双击的时间内点击了其他节点则需要根据最后一个处理一下 否则会出现当前节点单击命令被执行两次情况
        # 优先执行具体的鼠标点击事件
        if mouse_event == QtCore.Qt.RightButton:
            self.__run_action_chain(widgetItem, col, ["rbtnClick", "click"])
        elif mouse_event == QtCore.Qt.MiddleButton:
            self.__run_action_chain(widgetItem, col, ["mbtnClick", "click"])
        else:
            self.__run_action_chain(widgetItem, col, ["lbtnClick" ,"click"])
    # 执行事件链  ================================
    def __run_action_chain(self, widgetItem, col, signalTypes, params=None):
        """执行事件链,考虑问题如下
            1. 每个事件的参数传递可能是不同的需要考虑,但是传递给最终函数一定是个字典,使用者通过字典获取相关参数信息
                例如: params = {
                    widgetItem:widgetItem, 
                    col:col, mouseEvent:"lbtnClick",
                    srcWidgetItem:
                }
            2. 事件的执行顺序
                1. 自定义cell自身事件
                2. 节点列  绑定的点击事件 lbtnClick -> click (先执行“左键点击”事件然后在执行“点击”事件)
                3. 节点    绑定的点击事件 lbtnClick -> click
                4. 父节点    ...
                5. 域        ...
                6. 列头      ...
                7. 树        ...
            3. 注意这里不实际调用外部函数,是链的启动机,所以只需要传递当前点击节点以及需要触发的事件即可,具体的函数调用则由各个事件本身处理
        """

        gid = widgetItem.data(0, QtCore.Qt.UserRole) # 获取自动生成的gid 注意默认绑定在第0列
        # 获取当前节点的事件
        events = self._gid_events(gid) 
        field = self._col_to_field(col)
        if not params or not params.get("clickItem"):
            """如果没有传递参数则初始化参数,如果已经有则说明是当前为冒泡触发事件,要将当前的widgetItem加入到链中"""
            params = {
                **(params or {}),
                "clickItem":widgetItem,    # 点击的节点
                "chainItems":[widgetItem], # 执行事件链途径的节点
                "col":col, # 列
                "clickItemData": self._gid_data(gid) # 自带点击节点绑定的数据信息, 其他途径节点的数据可以通过 _gid_data 自行获取
            }
        else:
            chainItems = params.get("chainItems", [])
            chainItems.append(widgetItem)
            params['chainItems'] = chainItems
        if field in events:
            """判断是否存在cell绑定事件"""
            curFieldEvents = events.get(field, {}) 
            isStop = self._run_signal_action(signalTypes, lambda signalType, curFieldEvents=curFieldEvents: curFieldEvents.get(signalType), params)
            if isStop: return 
        # 获取当前列的事件
        isStop = self._run_signal_action(signalTypes, lambda signalType, events=events: events.get(signalType), params)
        if isStop: return 
        # 先判断当前节点是否为根节点,如果是根节点则执行列头事件然后是tree的事件,否则向上冒泡
        if not widgetItem.parent():
            """通过有没有值判断是否为根节点"""
            curColHeaderEvents = self._col_to_event(col) or {}
            isStop = self._run_signal_action(signalTypes, lambda signalType, curColHeaderEvents=curColHeaderEvents: curColHeaderEvents.get(signalType), params)
            if isStop: return 
            isStop = self._run_signal_action(signalTypes, lambda signalType: self._get_key_action(signalType), params)
            if isStop: return 
        else:
            return self.__run_action_chain(widgetItem.parent(), col, signalTypes, params = params)
    def _run_signal_action(self, signalTypes, getFun, params):
        status = False
        index = 0
        lenSignalTypes = len(signalTypes)
        if lenSignalTypes == 0:
            """没有要执行的信号类型则退出"""
            return status
        while not status and index < lenSignalTypes:
            fun  = getFun(signalTypes[index])
            if fun and utils.is_fun(fun):
                """判断是否存在当前事件"""
                status = fun(params)
            index += 1
        return status
    # 销毁  =====================================
    def _destroy(self):
        """销毁树
            1. 清理已关联的信号
            2. 清空可用信号列表
            3. 清空绑定事件
            4. 执行上级的销毁函数
        """
        # 清理信号
        self._disconnect_singals()
        # 清空可用信号列表
        self._clear_enable_signals()
        # 清空绑定事件
        self._clear_actions()
        # 执行上级的销毁函数
        super()._destroy()

if __name__ == '__main__':
    """测试当前模块"""
    import sys
    import qdarkstyle
    from qdarkstyle.light.palette import LightPalette
    from qdarkstyle.dark.palette import DarkPalette
    def get_headers():
        return [
            {
                "title": "名称",
                "field": "name",
                "type": "lable" # 缺省默认
            },
            {
                "title": "类型",
                "field": "type",
                "type": "lable" # 缺省默认
            },
            {
                "title": "是否激活",
                "field": "enable",
                "type": "checkbox"
            },
        ]
    def get_test_data():
        """根据header创建测试数据"""
        return [
            {
                "name": "test1", "type": "type1", "enable": True,
                "data":[1,2,3],
                "events":{
                    "lbtnClick": lambda *args, **kwargs: print("lbtnClick test1", ),
                    "rmenu": lambda *args, **kwargs: print("rmenu test1", ),
                    "name":{
                        "lbtnClick": lambda *args, **kwargs: print("lbtnClick test1 name", ),
                        "rbtnClick": lambda *args, **kwargs: print("rbtnClick test1 name", ),
                    }
                },
                "children":[
                    {"name": "test3", "type": "type3", "enable": True,
                        "children":[
                            {"name": "test5", "type": "type5", "enable": True},
                            {"name": "test6", "type": "type6", "enable": True},
                        ]
                    },
                    {"name": "test4", "type": "type4", "enable": False},
                ]
            },
            {"name": "test2", "type": "type2", "enable": False},
        ]
    app = QtWidgets.QApplication(sys.argv)
    tree = OpTree()
    # 注意如果不启用事件 那么跟调用DataTree类就没啥区别了
    #  ["itemClicked", "itemDoubleClicked", "itemActivated", "itemPressed", "currentItemChanged", "itemSelectionChanged", "customContextMenuRequested"]  #  "itemSelectionChanged", "currentItemChanged" 等事件

    tree._start_enable_signals(["itemClicked", "customContextMenuRequested"],useAll=False)
    tree.resize(800, 600)
    tree._set_headers(get_headers())
    tree._set_data(get_test_data())
    tree.setStyleSheet(qdarkstyle.load_stylesheet(qt_api="pyqt5", palette=DarkPalette))
    # 初始化tree的行为要放在初始化数据之后,因为每次初始化数据时都会先clear下数据以及定义好的事件
    tree.show()
    sys.exit(app.exec_())

